﻿using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace Devonline.Identity;

/// <summary>
/// Creates a new instance of a persistence store for the specified user type.
/// </summary>
/// <typeparam name="TUser">The type representing a user.</typeparam>
public class ResourceAccessStore : ResourceAccessStore<User, Resource, Level, AccessRule, DbContext, string>
{
    /// <summary>
    /// Constructs a new instance of <see cref="UserStore{TUser}"/>.
    /// </summary>
    /// <param name="context">The <see cref="DbContext"/>.</param>
    /// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
    public ResourceAccessStore(DbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { }
}

/// <summary>
/// Represents a new instance of a persistence store for the specified user and role types.
/// </summary>
/// <typeparam name="TUser">The type representing a user.</typeparam>
/// <typeparam name="TRole">The type representing a role.</typeparam>
/// <typeparam name="TContext">The type of the data context class used to access the store.</typeparam>
/// <typeparam name="TKey">The type of the primary key for a role.</typeparam>
public class ResourceAccessStore<TUser, TResource, TLevel, TAccessRule, TContext, TKey> :
    IResourceStore<TResource, TKey>,
    ILevelStore<TResource, TKey>,
    IAccessRuleStore<TAccessRule, TKey>,
    IResourceAccessRuleStore<TResource, TKey>,
    IAccessApplyStore<TUser, TResource, TKey>,
    IAccessRecordStore<TUser, TResource, TKey>
    where TKey : IEquatable<TKey>, IConvertible
    where TUser : User<TKey>
    where TResource : Resource<TKey>
    where TLevel : Level<TKey>
    where TAccessRule : AccessRule<TKey>, new()
    where TContext : DbContext
{
    /// <summary>
    /// Constructs a new instance of <see cref="UserStore{TUser, TRole, TContext, TKey}"/>.
    /// </summary>
    /// <param name="context">The <see cref="DbContext"/>.</param>
    /// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
    public ResourceAccessStore(TContext context, IdentityErrorDescriber describer = null)
    {
        Context = context;
        ErrorDescriber = describer;
    }

    private bool _disposed;
    /// <summary>
    /// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called.
    /// </summary>
    /// <value>
    /// True if changes should be automatically persisted, otherwise false.
    /// </value>
    public bool AutoSaveChanges { get; set; } = true;
    protected virtual TContext Context { get; }
    protected virtual IdentityErrorDescriber ErrorDescriber { get; }
    protected DbSet<TResource> Resources => Context.Set<TResource>();
    protected DbSet<TAccessRule> AccessRules => Context.Set<TAccessRule>();
    /// <summary>Saves the current store.</summary>
    /// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
    /// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
    protected Task SaveChanges(CancellationToken cancellationToken) => AutoSaveChanges ? Context.SaveChangesAsync(cancellationToken) : Task.CompletedTask;

    #region implement from IIdentityStore
    /// <summary>
    /// find a resource by id
    /// </summary>
    /// <param name="id"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task<TResource> FindByIdAsync(TKey id, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (id.Equals(default))
        {
            throw new ArgumentNullException(nameof(id));
        }

        return Resources.FindAsync(new object[] { id }, cancellationToken).AsTask();
    }
    /// <summary>
    /// find a resource by name
    /// </summary>
    /// <param name="name"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task<TResource> FindByNameAsync(string name, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (name.IsNullOrWhiteSpace())
        {
            throw new ArgumentNullException(nameof(name));
        }

        return Resources.FirstOrDefaultAsync(x => x.Name == name, cancellationToken);
    }
    /// <summary>
    /// set resource name
    /// </summary>
    /// <param name="resource"></param>
    /// <param name="name"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task SetName(TResource resource, string name, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();
        if (resource == null)
        {
            throw new ArgumentNullException(nameof(resource));
        }

        resource.Name = name;
        return Task.CompletedTask;
    }
    /// <summary>
    /// set resource alice
    /// </summary>
    /// <param name="resource"></param>
    /// <param name="alias"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task SetAlias(TResource resource, string alias, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();
        if (resource == null)
        {
            throw new ArgumentNullException(nameof(resource));
        }

        resource.Alias = alias;
        return Task.CompletedTask;
    }
    /// <summary>
    /// set resource image
    /// </summary>
    /// <param name="resource"></param>
    /// <param name="image"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task SetImage(TResource resource, string image, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();
        if (resource == null)
        {
            throw new ArgumentNullException(nameof(resource));
        }

        resource.Image = image;
        return Task.CompletedTask;
    }
    #endregion

    #region implement from IResourceStore
    /// <summary>
    /// get resources by owner id
    /// </summary>
    /// <param name="ownerId"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<IList<TResource>> GetByOwnerAsync(TKey ownerId, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (ownerId.Equals(default))
        {
            throw new ArgumentNullException(nameof(ownerId));
        }

        return await Resources.Where(x => x.OwnerId.Equals(ownerId)).ToListAsync(cancellationToken);
    }
    /// <summary>
    /// get children resources by current resource id
    /// </summary>
    /// <param name="id"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<IList<TResource>> GetChildrenResourcesAsync(TKey id, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (id.Equals(default))
        {
            throw new ArgumentNullException(nameof(id));
        }

        return await Resources.Where(x => x.ParentId.Equals(id)).ToListAsync(cancellationToken);
    }
    /// <summary>
    /// get parent resource of current resource
    /// </summary>
    /// <param name="id"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<TResource> GetParentResourceAsync(TKey id, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (id.Equals(default))
        {
            throw new ArgumentNullException(nameof(id));
        }

        var resource = await FindByIdAsync(id, cancellationToken);
        if (resource == null)
        {
            throw new ArgumentException($"the id of resource {id} not found!");
        }

        return await Resources.FirstOrDefaultAsync(x => x.Id.Equals(resource.ParentId), cancellationToken);
    }
    /// <summary>
    /// get the resources that user has been authorized
    /// </summary>
    /// <param name="userId"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public virtual Task<IList<TResource>> GetUserAuthorizedResourcesAsync(TKey userId, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (userId.Equals(default))
        {
            throw new ArgumentNullException(nameof(userId));
        }

        throw new NotImplementedException();
    }
    /// <summary>
    /// set resource owner
    /// </summary>
    /// <param name="resource"></param>
    /// <param name="ownerId"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public virtual Task SetOwner(TResource resource, TKey ownerId, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (resource == null)
        {
            throw new ArgumentNullException(nameof(resource));
        }

        resource.OwnerId = ownerId;
        return Task.CompletedTask;
    }
    /// <summary>
    /// set resource parent
    /// </summary>
    /// <param name="resource"></param>
    /// <param name="parentId"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public virtual Task SetParent(TResource resource, TKey parentId, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (resource == null)
        {
            throw new ArgumentNullException(nameof(resource));
        }

        resource.ParentId = parentId;
        return Task.CompletedTask;
    }
    /// <summary>
    /// set resource content
    /// </summary>
    /// <param name="resource"></param>
    /// <param name="content"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public virtual Task SetContent(TResource resource, string content, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();
        if (resource == null)
        {
            throw new ArgumentNullException(nameof(resource));
        }

        resource.Content = content;
        return Task.CompletedTask;
    }
    /// <summary>
    /// set resource type
    /// </summary>
    /// <param name="resource"></param>
    /// <param name="resourceType"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task SetResourceType(TResource resource, ResourceType resourceType, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (resource == null)
        {
            throw new ArgumentNullException(nameof(resource));
        }

        resource.ResourceType = resourceType;
        return Task.CompletedTask;
    }
    /// <summary>
    /// set resource access level
    /// </summary>
    /// <param name="resource"></param>
    /// <param name="accessLevel"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task SetAccessLevel(TResource resource, AccessLevel accessLevel, CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();
        if (resource == null)
        {
            throw new ArgumentNullException(nameof(resource));
        }

        resource.AccessLevel = accessLevel;
        return Task.CompletedTask;
    }
    /// <summary>
    /// 设置资源级别
    /// </summary>
    /// <param name="resource"></param>
    /// <param name="level"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task SetLevel(TResource resource, string level, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();
        if (resource == null)
        {
            throw new ArgumentNullException(nameof(resource));
        }

        var existLevel = await Context.Set<TLevel>().FirstOrDefaultAsync(x => x.Name == level, cancellationToken);
        if (existLevel == null)
        {
            throw new ArgumentNullException(nameof(level));
        }

        resource.LevelId = existLevel.Id;
    }
    #endregion

    #region implement from IAccessRuleStore
    /// <summary>
    /// 获取当前资源的所有访问规则
    /// </summary>
    /// <param name="resourceId"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<IList<TAccessRule>> GetByResourceIdAsync(TKey resourceId, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (resourceId.Equals(default))
        {
            throw new ArgumentNullException(nameof(resourceId));
        }

        return await AccessRules.Where(x => resourceId.Equals(x.ResourceId)).ToListAsync(cancellationToken);
    }
    /// <summary>
    /// 按授权对象获取所有访问规则列表
    /// </summary>
    /// <param name="identityId"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public async Task<IList<TAccessRule>> GetByIdentityIdAsync(TKey identityId, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (identityId.Equals(default))
        {
            throw new ArgumentNullException(nameof(identityId));
        }

        return await AccessRules.Where(x => identityId.Equals(x.IdentityId)).ToListAsync(cancellationToken);
    }
    /// <summary>
    /// 允许一个访问规则
    /// </summary>
    /// <param name="accessRule"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task Allow(TAccessRule accessRule, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();
        if (accessRule == null)
        {
            throw new ArgumentNullException(nameof(accessRule));
        }

        accessRule.IsAllow = AllowType.Allow;
        return Task.CompletedTask;
    }
    /// <summary>
    /// 禁止一个访问规则
    /// </summary>
    /// <param name="accessRule"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task Forbid(TAccessRule accessRule, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();
        if (accessRule == null)
        {
            throw new ArgumentNullException(nameof(accessRule));
        }

        accessRule.IsAllow = AllowType.Forbid;
        return Task.CompletedTask;
    }
    /// <summary>
    /// 设置访问规则的优先级
    /// </summary>
    /// <param name="accessRule"></param>
    /// <param name="priority"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task SetPriority(TAccessRule accessRule, int priority, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();
        if (accessRule == null)
        {
            throw new ArgumentNullException(nameof(accessRule));
        }

        accessRule.Priority = priority;
        return Task.CompletedTask;
    }
    /// <summary>
    /// 设置访问规则的额外条件
    /// </summary>
    /// <param name="accessRule"></param>
    /// <param name="condition"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task SetCondition(TAccessRule accessRule, string condition, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();
        if (accessRule == null)
        {
            throw new ArgumentNullException(nameof(accessRule));
        }

        accessRule.Condition = condition;
        return Task.CompletedTask;
    }
    #endregion

    #region implement from IResourceAccessRuleStore
    /// <summary>
    /// 对当前访问对象添加访问资源的权限
    /// </summary>
    /// <typeparam name="TIdentity"></typeparam>
    /// <param name="identity"></param>
    /// <param name="resource"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public virtual async Task<IdentityResult> AddAccessRuleAsync(IIdentity<TKey> identity, TResource resource, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (identity == null || identity.Id.Equals(default))
        {
            throw new ArgumentNullException(nameof(identity));
        }

        if (resource == null || resource.Id.Equals(default))
        {
            throw new ArgumentNullException(nameof(resource));
        }

        var identityType = identity.GetIdentityType();
        if (await AccessRules.AnyAsync(x => x.IdentityType == identityType && identity.Id.Equals(x.IdentityId) && resource.Id.Equals(x.ResourceId), cancellationToken))
        {
            return IdentityResult.Failed(identity switch
            {
                User<TKey> => ErrorDescriber.UserAlreadyInAccessRule(identity.Name, resource.Name),
                Role<TKey> => ErrorDescriber.RoleAlreadyInAccessRule(identity.Name, resource.Name),
                Group<TKey> => ErrorDescriber.GroupAlreadyInAccessRule(identity.Name, resource.Name),
                _ => ErrorDescriber.DefaultError()
            });
        }

        var accessRule = new TAccessRule
        {
            ResourceId = resource.Id,
            IdentityId = identity.Id,
            IdentityType = identity.GetIdentityType()
        };

        accessRule.Create();
        accessRule.Update();

        try
        {
            Context.Add(accessRule);
            await SaveChanges(cancellationToken);
        }
        catch (Exception)
        {
            return IdentityResult.Failed(ErrorDescriber.DefaultError());
        }

        return IdentityResult.Success;
    }
    /// <summary>
    /// 对当前访问对象添加访问资源列表的权限
    /// </summary>
    /// <typeparam name="TIdentity"></typeparam>
    /// <param name="identity"></param>
    /// <param name="resources"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public virtual async Task<IdentityResult> AddAccessRulesAsync(IIdentity<TKey> identity, IList<TResource> resources, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (identity == null || identity.Id.Equals(default))
        {
            throw new ArgumentNullException(nameof(identity));
        }

        if (resources.IsNullOrEmpty() || resources.Any(x => x.Id.Equals(default)))
        {
            throw new ArgumentNullException(nameof(resources));
        }

        var identityType = identity.GetIdentityType();
        var resoourceIds = await AccessRules.Where(x => x.IdentityType == identityType && identity.Id.Equals(x.IdentityId) && resources.Select(a => a.Id).Contains(x.ResourceId)).Select(x => x.ResourceId).ToListAsync(cancellationToken);
        if (resoourceIds.IsNotNullOrEmpty())
        {
            resources = resources.Where(x => !resoourceIds.Contains(x.Id)).ToList();
        }

        if (resources.IsNotNullOrEmpty())
        {
            try
            {
                foreach (var resource in resources)
                {
                    var accessRule = new TAccessRule
                    {
                        ResourceId = resource.Id,
                        IdentityType = identityType,
                        IdentityId = identity.Id
                    };

                    accessRule.Create();
                    accessRule.Update();

                    Context.Add(accessRule);
                }

                await SaveChanges(cancellationToken);
            }
            catch (Exception)
            {
                return IdentityResult.Failed(ErrorDescriber.DefaultError());
            }
        }

        return IdentityResult.Success;
    }
    /// <summary>
    /// 对当前访问对象移除资源访问的权限
    /// </summary>
    /// <typeparam name="TIdentity"></typeparam>
    /// <param name="identity"></param>
    /// <param name="resource"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public virtual async Task<IdentityResult> RemoveAccessRuleAsync(IIdentity<TKey> identity, TResource resource, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (identity == null || identity.Id.Equals(default))
        {
            throw new ArgumentNullException(nameof(identity));
        }

        if (resource == null || resource.Id.Equals(default))
        {
            throw new ArgumentNullException(nameof(resource));
        }

        var identityType = identity.GetIdentityType();
        var accessRule = await AccessRules.FirstOrDefaultAsync(x => x.IdentityType == identityType && identity.Id.Equals(x.IdentityId) && resource.Id.Equals(x.ResourceId), cancellationToken);
        if (accessRule == null)
        {
            return IdentityResult.Failed(identity switch
            {
                User<TKey> => ErrorDescriber.UserNotInAccessRule(identity.Name, resource.Name),
                Role<TKey> => ErrorDescriber.RoleNotInAccessRule(identity.Name, resource.Name),
                Group<TKey> => ErrorDescriber.GroupNotInAccessRule(identity.Name, resource.Name),
                _ => ErrorDescriber.DefaultError()
            });
        }

        try
        {
            Context.Remove(accessRule);
            await SaveChanges(cancellationToken);
        }
        catch (DbUpdateConcurrencyException)
        {
            return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
        }

        return IdentityResult.Success;
    }
    /// <summary>
    /// 对当前访问对象移除列表中资源访问的权限
    /// </summary>
    /// <typeparam name="TIdentity"></typeparam>
    /// <param name="identity"></param>
    /// <param name="resources"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public virtual async Task<IdentityResult> RemoveAccessRulesAsync(IIdentity<TKey> identity, IList<TResource> resources, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ThrowIfDisposed();

        if (identity == null || identity.Id.Equals(default))
        {
            throw new ArgumentNullException(nameof(identity));
        }

        if (resources.IsNullOrEmpty() || resources.Any(x => x.Id.Equals(default)))
        {
            throw new ArgumentNullException(nameof(resources));
        }

        var identityType = identity.GetIdentityType();
        var accessRules = await AccessRules.Where(x => x.IdentityType == identityType && identity.Id.Equals(x.IdentityId) && resources.Select(a => a.Id).Contains(x.ResourceId)).ToListAsync(cancellationToken);
        if (accessRules.IsNotNullOrEmpty())
        {
            AccessRules.RemoveRange(accessRules);

            try
            {
                await SaveChanges(cancellationToken);
            }
            catch (DbUpdateConcurrencyException)
            {
                return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
            }
        }

        return IdentityResult.Success;
    }
    #endregion

    /// <summary>
    /// TODO
    /// </summary>
    /// <param name="user"></param>
    /// <param name="resource"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task Apply(TUser user, TResource resource, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
    /// <summary>
    /// TODO
    /// </summary>
    /// <param name="user"></param>
    /// <param name="resource"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    public Task Record(TUser user, TResource resource, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    /// <summary>
    /// Throws if this class has been disposed.
    /// </summary>
    protected void ThrowIfDisposed()
    {
        if (_disposed)
        {
            throw new ObjectDisposedException(GetType().Name);
        }
    }
    /// <summary>
    /// Dispose the stores
    /// </summary>
    public void Dispose()
    {
        _disposed = true;
        GC.SuppressFinalize(this);
    }
}